LayoutSizeMode = Enum {
    "Custom",
    "Stretch",
    "StretchX",
    "StretchY",
}
LayoutChildAlign = Enum {
    "Start",
    "Center",
    "End"
}

local function checkAddTag(self, tag)
    if self:hasTag(tag) then
        Console.error("already has tag ", tag)
        return
    end
    self:addTag(tag)
end
local function updateLayoutSizeTags(self, sizeMode)
    if self._layoutSizeMode == LayoutSizeMode.Stretch then
        self:removeTags("WidthController", "HeightController")
    elseif self._layoutSizeMode == LayoutSizeMode.StretchX then
        self:removeTag("WidthController")
    elseif sizeMode == LayoutSizeMode.StretchY then
        self:removeTag("HeightController")
    end

    if sizeMode == LayoutSizeMode.Stretch then
        checkAddTag(self, "WidthController")
        checkAddTag(self, "HeightController")
    elseif sizeMode == LayoutSizeMode.StretchX then
        checkAddTag(self, "WidthController")
    elseif sizeMode == LayoutSizeMode.StretchY then
        checkAddTag(self, "HeightController")
    end
end

Layout = {
    props = {
        _layoutType = LayoutType.Vertical,
        _layoutSizeMode = LayoutSizeMode.Custom,
        _topPadding = 0,
        _leftPadding = 0,
        _bottomPadding = 0,
        _rightPadding = 0,
        _space = 0,
        _childAlignX = LayoutChildAlign.Start,
        _childAlignY = LayoutChildAlign.Start,
        layoutDirty = true,
        layoutSizeDirty = false,
    },
    gets = {
        layoutType = function (self)
            return self._layoutType
        end,
        childAlignX = function (self)
            return self._childAlignX
        end,
        childAlignY = function (self)
            return self._childAlignY
        end,
        layoutSizeMode = function (self)
            return self._layoutSizeMode
        end,
        space = function (self)
            return self._space
        end
    },
    sets = {
        layoutType = function (self, key, val)
            self._layoutType = val
            self:setLayoutDirty()
            if self._layoutSizeMode ~= LayoutSizeMode.Custom then
                self:setLayoutSizeDirty()
            end
        end,
        childAlignX = function (self, key, val)
            self._childAlignX = val
            self:setLayoutDirty()
        end,
        childAlignY = function (self, key, val)
            self._childAlignY = val
            self:setLayoutDirty()
        end,
        layoutSizeMode = function (self, key, val)
            updateLayoutSizeTags(self, val)
            self._layoutSizeMode = val
            if val ~= LayoutSizeMode.Custom then
                self:setLayoutSizeDirty()
            end
        end,
        space = function (self, key, val)
            self._space = val
            self:setLayoutDirty()
        end
    },
}

---@diagnostic disable-next-line: duplicate-set-field
function Layout:ctor()
    self:on(NodeEvent.AddChild, self._onAddChild, self)
    self:on(NodeEvent.RemoveChild, self._onRemoveChild, self)
    for _, c in ipairs(self.children) do
        self:_onAddChild(c)
    end
end

for _, k in ipairs({ "topPadding", "leftPadding", "rightPadding", "bottomPadding" }) do
    Layout.gets[k] = load([[
        return function(self, key)
            return self._]] .. k .. [[
        end
    ]])()

    Layout.sets[k] = load([[
        return function(self, key, val)
            self._]] .. k .. [[ = val
            self:setLayoutDirty()
            if self._layoutSizeMode ~= LayoutSizeMode.Custom then
                self:setLayoutSizeDirty()
            end
        end
    ]])()
end

function Layout:_onAddChild(child)
    if child:hasTag("XController") or child:hasTag("YController") then
        Console.error("Layout:addChild", "child", child.name, "has tag XController or YController", "which is reserved for layout")
    end
    child:addTags("XController", "YController")
    self:setLayoutDirty()
    if self._layoutSizeMode ~= LayoutSizeMode.Custom then
        self:setLayoutSizeDirty()
    end
    child:on(NodeEvent.WidthDirty, self._onChildWidthChanged, self)
    child:on(NodeEvent.HeightDirty, self._onChildHeightChanged, self)
end

function Layout:_onRemoveChild(child)
    child:removeTags("XController", "YController")
    self:setLayoutDirty()
    if self._layoutSizeMode ~= LayoutSizeMode.Custom then
        self:setLayoutSizeDirty()
    end
    child:off(NodeEvent.WidthDirty, self._onChildWidthChanged, self)
    child:off(NodeEvent.HeightDirty, self._onChildHeightChanged, self)
end

function Layout:_onChildWidthChanged()
    if self._layoutSizeMode == LayoutSizeMode.Stretch or self._layoutSizeMode == LayoutSizeMode.StretchX then
        self:setLayoutSizeDirty()
    end
    self:setLayoutDirty()
end

function Layout:_onChildHeightChanged()
    if self._layoutSizeMode == LayoutSizeMode.Stretch or self._layoutSizeMode == LayoutSizeMode.StretchY then
        self:setLayoutSizeDirty()
    end
    self:setLayoutDirty()
end

function Layout:setLayoutSizeDirty()
    self.layoutSizeDirty = true
    if self.layoutSizeMode == LayoutSizeMode.StretchX then
        self:emit(NodeEvent.WidthDirty)
    elseif self.layoutSizeMode == LayoutSizeMode.StretchY then
        self:emit(NodeEvent.HeightDirty)
    end
end

function Layout:setLayoutDirty()
    self.layoutDirty = true
end

function Layout:updateLayoutSize()
    if self._layoutSizeMode == LayoutSizeMode.Stretch then
        self:upadteStretch()
    elseif self._layoutSizeMode == LayoutSizeMode.StretchX then
        self:upadteStretchX()
    elseif self._layoutSizeMode == LayoutSizeMode.StretchY then
        self:updateStretchY()
    end
    self.layoutSizeDirty = false
end

function Layout:updateLayout()
    if self.layoutType == LayoutType.Vertical then
        self:_updateLayoutVertical()
    else
        self:_updateLayoutHorizontal()
    end
    self.layoutDirty = false
end

function Layout:upadteStretchX()
    local wCount = 0
    for _, c in ipairs(self.children) do
        if c.active then
            if self.layoutType == LayoutType.Horizontal then
                wCount = wCount + c.width * c.sx
            else
                wCount = math.max(wCount, c.width * c.sx)
            end
        end
    end
    self.width = wCount + self.leftPadding + self.rightPadding + self._space * (#self.children - 1)
end

function Layout:updateStretchY()
    local hCount = 0
    for _, c in ipairs(self.children) do
        if c.active then
            if self.layoutType == LayoutType.Horizontal then
                hCount = math.max(hCount, c.height * c.sy)
            else
                hCount = hCount + c.height * c.sy
            end
        end
    end
    self.height = hCount + self.topPadding + self.bottomPadding + self._space * (#self.children - 1)
end

function Layout:upadteStretch()
    local wCount = 0
    local hCount = 0
    for _, c in ipairs(self.children) do
        if c.active then
            if self.layoutType == LayoutType.Horizontal then
                wCount = wCount + c.width * c.sx
                hCount = math.max(hCount, c.height * c.sy)
            else
                wCount = math.max(wCount, c.width * c.sx)
                hCount = hCount + c.height * c.sy
            end
        end
    end
    if self.layoutType == LayoutType.Horizontal then
        wCount = wCount + (#self.children - 1) * self._space
    else
        hCount = hCount + (#self.children - 1) * self._space
    end
    self.width = wCount + self.leftPadding + self.rightPadding
    self.height = hCount + self.topPadding + self.bottomPadding
end

function Layout:_updateLayoutVertical()
    local x
    local left = self:localLeft()
    local xfn
    if self.childAlignX == LayoutChildAlign.Start then
        x = left + self._leftPadding
        xfn = "left"
    elseif self.childAlignX == LayoutChildAlign.Center then
        x = left + self._leftPadding + (self.width - self._leftPadding - self._rightPadding) * 0.5
        xfn = "xCenter"
    elseif self.childAlignX == LayoutChildAlign.End then
        x = left + self.width - self._rightPadding
        xfn = "right"
    end
    local top = self:localTop() + self.topPadding
    for _, c in ipairs(self.children) do
        if c.active then
            c[xfn](c, x)
            c:top(top)
            top = top + c.height * c.sy + self._space
        end
    end
end

function Layout:_updateLayoutHorizontal()
    local y
    local top = self:localTop() + self._topPadding
    local left = self:localLeft() + self._leftPadding
    local yfn
    if self.childAlignY == LayoutChildAlign.Start then
        y = top
        yfn = "top"
    elseif self.childAlignY == LayoutChildAlign.Center then
        y = top + (self.height - self._topPadding - self._bottomPadding) * 0.5
        yfn = "yCenter"
    elseif self.childAlignY == LayoutChildAlign.End then
        y = top + self.width - self._topPadding - self._bottomPadding
        yfn = "bottom"
    end
    for _, c in ipairs(self.children) do
        if c.active then
            c:left(left)
            c[yfn](c, y)
            left = left + c.width * c.sx + self._space
        end
    end
end

function Node.Layout()
    local node = Node.new()
    node.name = "layout"
    node:addComponent(Layout)
    return node
end
